Go 并发编程测试分析
Go 并发编程测试分析
本文是对https://colobu.com/2019/04/28/go-concurrency-quizzes/
晁岳攀老师博客中提到的并发测试例子的讲解。
我按问题类型来分类说明错误原因。
所以P中没有可以调度的G时就会出现死锁
这个问题涉及到的题目为:1、Mutex
,2、RWMutex
.
1、Mutex
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
var chain string
func main() {
chain = "main"
A()
fmt.Println(chain)
}
func A() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> A"
B()
}
func B() {
chain = chain + " --> B"
C()
}
func C() {
mu.Lock()
defer mu.Unlock()
chain = chain + " --> C"
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
这一题的问题是runtime
中只有一个G
,但是当运行到A()
方法是以及加锁,后面的C()
方法接着去加锁,就会出现拿不到锁,于是这个G
的状态就变为不可运行,所以就出现了deadlock!
.
2、RWMutex
package main
import (
"fmt"
"sync"
"time"
)
var mu sync.RWMutex
var count int
func main() {
go A()
time.Sleep(2 * time.Second)
mu.Lock()
defer mu.Unlock()
count++
fmt.Println(count)
}
func A() {
mu.RLock()
defer mu.RUnlock()
B()
}
func B() {
time.Sleep(5 * time.Second)
C()
}
func C() {
mu.RLock()
defer mu.RUnlock()
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
这一题的原因也是一样的,G
A休眠之后状态就会变为等待,此时,主G
去那锁也没有那到,就会变为不可运行状态,并让出cpu
,此时所有的G
都不可运行就出现死锁了。
WaitGroup 使用问题
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(time.Millisecond)
wg.Done()
wg.Add(1)
}()
wg.Wait()
}
$ go run main.go
panic: sync: WaitGroup is reused before previous Wait has returned
原因是多调用了一个wg.Add(1)
。
4、双检查实现单例
package doublecheck
import (
"sync"
)
type Once struct {
m sync.Mutex
done uint32
}
func (o *Once) Do(f func()) {
if o.done == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
o.done = 1
f()
}
}
这一题争议最大,博主是想告诉我们类似于Java
里面缓存变量的问题,我觉得这里是不会出现问题的,因为对象是指针调用;运行本身没有问题,但是会出现数据竞争。使用go run -race main.go
运行就会发现有数据竞争。
5、同步对象使用后不能被拷贝
package main
import (
"fmt"
"sync"
)
type MyMutex struct {
count int
sync.Mutex
}
func main() {
var mu MyMutex
mu.Lock()
var mu2 = mu
mu.count++
mu.Unlock()
mu2.Lock()
mu2.count++
mu2.Unlock()
fmt.Println(mu.count, mu2.count)
}
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
这个原因就是同步对象使用过之后不能再被拷贝,如果上面把mu.Lock()
和var mu2 = mu
这两行进行交换一下就可以了。
使用过后不可以复制的对象有:
// A Cond must not be copied after first use.
type Cond struct
// A Map must not be copied after first use.
type Map struct
// A Mutex must not be copied after first use.
type Mutex struct
// A Pool must not be copied after first use.
type Pool struct
// A RWMutex must not be copied after first use.
type RWMutex struct
// A WaitGroup must not be copied after first use.
type WaitGroup struct
sync
包下的struct
除了Once
这个结构体其他的使用过后都不能被复制。不能被复制也包括函数传递参数,比如如下的使用是错误的:
func main{
var wa sync.WaitGroup
for i:=0 ;i < 10 ;i++ {
wa.Add(1)
go func(wa sync.WaitGroup){
fmt.Println("wa.Down()")
wa.Down()
}(wa)
}
wa.Wait()
}
上面的代码就有问题,首先wa
对象以及使用了wa.Add(1)
,后面开启一个go
时确做参数传入,此时传入的是一个副本,就会出现不能正确的执行wa.Down()
;此处可以修改为传递指针go func(wa *sync.WaitGroup)
或者使用闭包的方式使用WaitGroup
对象。
7、channel
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var ch chan int
go func() {
ch = make(chan int, 1)
ch <- 1
}()
go func(ch chan int) {
time.Sleep(time.Second)
<-ch
}(ch)
c := time.Tick(1 * time.Second)
for range c {
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
}
}
$ go run main.go
#goroutines: 2
#goroutines: 2
#goroutines: 2
这一题比较简单,但是我还是忽略了一个关键点,最后运行中有两个g
,原因就是time.Tick
其实是开启了一个G
来计时的,然后通过Channel
来通知。
我们来看一下time.Tick
里面的实现:
time 包
func NewTicker(d Duration) *Ticker{
...
startTimer(&t.r)// 这个方法对应到runtime.startTimer()方法上
}
runtime 包
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
//在addtimer()方法中启动了一个`G`。
13、for range 问题
package main
import (
"fmt"
"sync"
"time"
)
type T struct {
V int
}
func (t *T) Incr(wg *sync.WaitGroup) {
t.V++
wg.Done()
}
func (t *T) Print() {
time.Sleep(1e9)
fmt.Print(t.V)
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
var ts = make([]T, 10)
for i := 0; i < 10; i++ {
ts[i] = T{i}
}
for _, t := range ts {
go t.Incr(&wg)
}
wg.Wait()
for _, t := range ts {
go t.Print()
}
time.Sleep(5 * time.Second)
}
$ go run main.go
999999999
这一题可以是因为使用了for range
的方式,这种方式中的t
只是一个变量,会一直在边,当使用go t.Incr()
的时候,此时的t
已经变为了最后一个值,所以输出都是9
,这个在使用map
时也有这个问题,得到 的k/v
只是值的一个拷贝。